<?php

	/**
	 * Defines a persistent object using PDO and Databases
	 * It supports, basically, PostgreSQL, MySQL, MSSQL and
	 * Oracle databases.
	 * Things get easy when all you need is:
	 * $obj = new Person();
	 * $obj->name = "John Doe";
	 * $obj->phone = 55555555;
	 * $obj->save();
	 *
	 * @author Pablo Santiago Sánchez <phackwer@gmail.com>
	 * @copyright Copyright (c) 2008, Pablo Santiago Sánchez
	 * @license http://opensource.org/licenses/bsd-license.php BSD License
	 * @package pop
	 * @subpackage core
	 */

	/**
	 * Class used as base for all persistent objects created under
	 * the POP paradigm
	 *
	 * @package pop
	 * @subpackage core
	 */
	abstract class Persist extends POPCore
	{
		/**
		 * Name of the database connection declared for metamapping
		 *
		 * @access protected
		 * @var string
		 */
		protected $db;

		/**
		 * Name of the schema on the database declared for metamapping
		 *
		 * @access protected
		 * @var string
		 */
		protected $schema;

		/**
		 * Name of the table on the database declared for metamapping
		 *
		 * @access protected
		 * @var string
		 */
		protected $table;

		/**
		 * Classes that contains this class (used for ORM)
		 *
		 * @access protected
		 * @var string
		 */
		protected $parent_classes;

		/**
		 * Metamapping of Attributes and Fields on the Database
		 * @access protected
		 * @var string
		 */
		protected $meta_mapping;

		/**
		 * this is the only thing that will be changed on classes that extends
		 * Persist __construct on extended classes must call the
		 * parent::__construct() method due to the fact that the data typing of
		 * attributes must be set in the constructor
		 */
		public function __construct()
		{
			//creates the ID attribute, the only one that cannot be set manually
			$this->id = new PInteger();

			//object must be initialized
			$this->initialize();
		}

		/**
		 * Magic method used to set the value and check if it's valid
		 *
		 * @param string $name name of the value
		 * @param string $value value itself
		 * @return bool
		 */
		public function __set($name, $value)
		{
			try
			{
				if (get_class($this->$name) != "PArrayOf")
				{
					$this->$name->value = trim($value);
					if ($this->pop_parent && isset($this->pop_parent->$name))
						$this->pop_parent->set($name,trim($value));
					return true;
				}
				else
				{
					POPDebug::exception("You cannot set the ID attribute manually. It must be given by the database.");
				}
			}
			catch (Exception $e)
			{
				POPDebug::exception("Error setting ".$name.": ".$e->getMessage());
			}
		}

		/**
		 * Magic method used to get the value of an attribute
		 *
		 * @param string $name name of the value
		 * @return mixed
		 */
		public function __get($name)
		{
			if ($this->checkAttrName($name))
			{
				if (isset($this->$name) && get_class($this->$name)=="PArrayOf")
					return $this->$name->array;
				else if (isset($this->$name))
					return $this->$name->value;
			}
			else
				return $this->$name;
		}

		/**
		 * Method used to set the value and check if it's valid
		 *
		 * @param string $name name of the value
		 * @param string $value value itself
		 * @return bool
		 */
		public function set($name, $value)
		{
			$this->__set($name, $value);
		}

		/**
		 * Method used to get the value of an attribute
		 *
		 * @param string $name name of the value
		 * @return mixed
		 */
		public function get($name)
		{
			return $this->__get($name);
		}

		/**
		 * Method for getting the datatype of an attribute
		 * @param $attr
		 * @return string
		 */
		public function getAttrType($attr)
		{
			if ($this->checkAttrName($attr))
				return get_class($this->$attr);
		}

		/**
		 * Method used to set the value of $id_self, used for extended classes
		 *
		 * @param array $arr
		 */
		public function setIdSelf($arr)
		{
			$idselfname = "id_self_".$this->pop_table;

			if (!isset($arr[$idselfname]))
				$idselfname = substr($idselfname, 0, 30);

			$this->id_self = $arr[$idselfname];
			$this->id->value = $this->id_self;

			if ($this->pop_parent_type != "Persist")
				$this->pop_parent->setIdSelf($arr);
		}

		/**
		 * Method for reseting PArrayOf
		 * @param $key
		 * @return unknown_type
		 */
		public function resetArray($key)
		{
			$this->$key->reset();
		}

		/**
		 * Method used to load the object with the id value passed, or
		 * can receive an array of attributes and values it should return
		 *
		 * @param mixed $value Integer of the ID on database or other criterias as an array
		 * @return mixed
		 */
		public function load($value)
		{
			try
			{
				$this->pop_db_driver->load($value, $this);
			}
			catch (Exception $e)
			{
				POPDebug::exception($e->getMessage());
			}
		}

		/**
		 * Method used to load the object and all associations with the id value passed, or
		 * can receive an array of attributes and values it should return
		 *
		 * @param mixed $value Integer of the ID on database or other criterias as an array
		 * @return mixed
		 */
		public function loadAll($value)
		{
			$this->load($value);
			foreach ($this->pop_attrs as $key=>$attr)
			{
				if ($this->getAttrType($key) == "PArrayOf")
				{
					$this->loadAllAssociated($key);
				}
			}
		}

		/**
		 * Method used to save the object on the database
		 *
		 * @return bool true on sucess, false and Exception on failure
		 */
		public function save()
		{
			try
			{
				$this->pop_db_driver->save($this);
			}
			catch (Exception $e)
			{
				POPDebug::exception($e->getMessage());
			}
		}

		/**
		 * Method used to save the object and all associations on the database
		 */
		public function saveAll()
		{
			$this->save();

			foreach ($this->pop_attrs as $key=>$attr)
			{
				if ($this->checkAttrName($key) && get_class($this->$key) == "PArrayOf")
				{
					$this->saveAllAssociated($key);
				}
			}
		}

		/**
		 * Method used to delete the object from the database
		 */
		public function delete()
		{
			try
			{
				$this->pop_db_driver->delete($this);
			}
			catch (Exception $e)
			{
				POPDebug::exception($e->getMessage());
			}
		}

		/**
		 * Method used to add an object to an attribtute that is a PArrayOf
		 *
		 * @param string $associated Name of the attribute that holds the PArrayOf
		 * @param mixed $element Name of the attribute that holds the PArrayOf
		 */
		public function addAssociated($associated, $element)
		{
			$this->$associated->add($element);
		}

		/**
		 * Method used to delete an object from a collection hold on a PArrayOf attribute
		 *
		 * @param string $associated Name of the attribute that holds the PArrayOf
		 * @param string $criteria Criteria used ("index"||"id")
		 * @param integer $value Value of the criteria
		 */
		public function delAssociated($associated,$criteria,$value)
		{
			if($criteria == "index")
			{
				$index = $value;
				if ($index < count($this->$associated->array) && $this->$associated->array[$index])
					$this->$associated->array[$index]->delete();
				else
				{
					POPDebug::exception("There is no object loaded on $associated with index: $index.");
				}
			}
			else if($criteria == "id")
			{
				$exists = false;
				foreach($this->$associated->array as $index => $obj)
				{
					if ($obj->id->value == $value)
					{
						$this->$associated->array[$index]->delete();
						$exists = true;
					}
				}
				if (!$exists)
				{
					POPDebug::exception("There is no object loaded on $associated with this id: $value.");
				}
			}

			$this->$associated->del($criteria,$value);
		}

		/**
		 * Method used to get an object from a collection hold on a PArrayOf attribute
		 *
		 * @param string $associated Name of the attribute that holds the PArrayOf
		 * @param string $criteria Criteria used ("index"||"id")
		 * @param integer $value Value of the criteria
		 */
		public function &getAssociated($associated,$criteria,$value)
		{
			if($criteria == "index")
			{
				$index = $value;
				if ($index < count($this->$associated->array) && $this->$associated->array[$index])
					return $this->$associated->array[$index];
				else
				{
					POPDebug::exception("There is no object loaded on $associated with index: $index.");
				}
			}
			else if($criteria == "id")
			{
				foreach($this->$associated->array as $index => $obj)
					if ($obj->id->value == $value)
						return $this->$associated->array[$index];

				//if it doens't find any, throw exception
				POPDebug::exception("There is no object loaded on $associated with this id: $value.");
			}
		}

		/**
		 * Internal Method used to get the name of an associated object
		 *
		 * @param object $obj Object used for the extraction
		 */
		public function getAssociatedName($obj)
		{
			if (in_array($this->pop_parent_type, $obj->parent_classes))
			{
				foreach ($obj->parent_classes as $classname)
				{
					if ($this->pop_parent_type == $classname)
						return strtolower(get_class($this->pop_parent));
					else
						return strtolower(get_class($this));
				}
			}
			else
			{
				if(in_array(get_class($this), $obj->parent_classes))
				{
					foreach ($obj->parent_classes as $classname)
						return strtolower(get_class($this));
				}
				else
					return strtolower($this->pop_parent->getAssociatedName($obj));
			}
		}

		/**
		 * Method used to get load an object to a collection hold on by a PArrayOf attribute
		 *
		 * @param string $associated Name of the attribute that holds the PArrayOf
		 */
		public function loadAssociated($associated)
		{
			try
			{
				$obj = new $this->$associated->objecttype();
				$associated_name = $this->getAssociatedName($obj);
				$id = $obj->getMetaName("id_".$associated_name);

				$id_value = $this->id->value;

				if (!$this->id->value && $this->pop_parent->id->value)
					$id_value = $this->pop_parent->id->value;

				$complement = ($this->pop_schema != '') ? "." : "";
				$sql = "select ".$obj->getMetaName("id")." as id from ".$complement.$obj->pop_table." where $id = ".$id_value;
				POPDebug::log($sql);
				$rs = POPDB::getConnection($obj->pop_db)->query($sql);
				$result = $rs->fetchAll();
				$rs->closeCursor();

				$this->$associated->reset();

				foreach ($result as $row)
				{
					$obj = new $this->$associated->objecttype();
					$obj->load($row["id"]);

					$this->$associated->add($obj);
				}
			}
			catch (Exception $e)
			{
				POPDebug::exception("Error: $associated\n".$e->getMessage());
			}
		}

		/**
		 * Method used to get load all objects to a collection hold on by a PArrayOf attribute
		 *
		 * @param string $associated Name of the attribute that holds the PArrayOf
		 */
		public function loadAllAssociated($associated)
		{
			$this->loadAssociated($associated);

			foreach ($this->$associated->array as $obj)
			{
				$obj->loadAll($obj->id->value);
			}
		}

		/**
		 * Method used to get save all objects hold on a collection by a PArrayOf attribute
		 *
		 * @param string $associated Name of the attribute that holds the PArrayOf
		 */
		public function saveAssociated($associated)
		{
			if (!$this->id->value)
			{
				POPDebug::exception("This object is not in the database yet.\nYou have to save it first to generate an id.");
			}

			$obj = new $this->$associated->objecttype();
			$associated_name = $this->getAssociatedName($obj);
			$id = "id_".$associated_name;

			foreach ($this->$associated->array as $obj)
			{
				foreach ($obj->parent_classes as $objparent)
				{
					$id_parent = "id_".strtolower($objparent);

					if (!$obj->$id->value)
						$obj->$id->value = $this->id->value;

					if(!$obj->pop_attrs[$id_parent]->value)
					{
						if($this->$id_parent->value)
							$obj->pop_attrs[$id_parent]->value = $this->$id_parent->value;
						else
						{
							POPDebug::exception("ID not set.");
						}
					}
				}

				$obj->save();
			}
		}

		/**
		 * Method used to get save all objects and associated objects hold on a collection
		 * by a PArrayOf attribute
		 *
		 * @param string $associated Name of the attribute that holds the PArrayOf
		 */
		public function saveAllAssociated($associated)
		{
			$obj = new $this->$associated->objecttype();
			$associated_name = $this->getAssociatedName($obj);
			$id = "id_".$associated_name;

			foreach ($this->$associated->array as $obj)
			{
				foreach ($obj->parent_classes as $objparent)
					if (isset($obj->$id) && !$obj->$id->value)
						$obj->$id->value = $this->id->value;

				$obj->saveAll();
			}
		}

		/**
		 * Method used to get search objects on the database, given the criterias
		 *
		 * @param array $return_attributes Array listing the attributes/columns you want to be returned
		 * @param array $search_attributes_and_values Criterias used for searching
		 * @param string $output_format Format of the return. Can be "PDO" or "XML". Default is PDO.
		 * @return mixed PDO Recordset Object or XML string
		 */
		public function search($return_attributes = null, $search_attributes_and_values = null, $output_format = null)
		{
			try
			{
				return $this->pop_db_driver->search($this, $return_attributes, $search_attributes_and_values, $output_format);
			}
			catch (Exception $e)
			{
				POPDebug::exception($e->getMessage());
			}
		}

		/**
		 * Converts the array to a JSON String, useful on WebServices
		 *
		 * @param string $attr Array of the attributes you want to be converted. null means you want all of them
		 * @param string $tab Internal use - for identation
		 * @param string $endcomma Internal use - for identation
		 * @return string
		 */
		public function toJSON($attr=null, $tab = "", $endcomma="")
		{
			$comma = "";
			$json = $tab."{";

			foreach ($this->pop_attrs as $key => $objvalue)
			{
				if ($this->checkAttrName($key) && (!$attr || ($attr && in_array($key,$attr))))
				{
					if ($objvalue instanceof PArrayOf)
					{
						$json .= $comma."\n".$tab."\t".$key." : [\n";
						$json .= $objvalue->toJSON($attr, $tab, $endcomma);
						$json .= $tab."\t]";
					}
					else if (isset($objvalue))
						$json .= $comma."\n".$tab."\t".$key ." : ".$objvalue->value;

					$comma = ",";
				}
			}
			$json .= "\n".$tab."}".$endcomma."\n}\n";

			return $json;
		}

		/**
		 * Converts the array to a XML String, useful on WebServices
		 *
		 * @param string $attr Array of the attributes you want to be converted. null means you want all of them
		 * @return string
		 */
		public function toXML($attr=null)
		{
			$xml = "<".get_class($this)." id=\"".$this->id->value."\">";

			foreach ($this->pop_attrs as $key => $objvalue)
			{
				if ($this->checkAttrName($key) && (!$attr || ($attr && in_array($key,$attr))))
				{
					if ($objvalue instanceof PArrayOf)
					{
						$xml .= "<".$key .">";
						$xml .= $objvalue->toXML($attr);
						$xml .= "</".$key .">";
					}
					else if (isset($objvalue))
						$xml .= "<".$key .">". $objvalue->value . "</".$key .">";
				}
			}

			$xml .= "</".get_class($this).">";

			return $xml;
		}

		/**
		 * Drops the table associated to the object from the database
		 */
		public function dropTable()
		{
			try
			{
				$this->pop_db_driver->dropTable($this->pop_db, $this->pop_schema, $this->pop_table);
			}
			catch (Exception $e)
			{
				POPDebug::exception($e->getMessage());
			}
		}

		/**
		 * Creates the table associated to the object on the database
		 */
		public function createTable()
		{
			try
			{
				$this->pop_db_driver->createTable($this);
			}
			catch (Exception $e)
			{
				POPDebug::exception($e->getMessage());
			}
		}

		/**
		 * Alters the table associated to the object on the database to the current object spacification.
		 * If the table doesn't exists, it tries to create it.
		 */
		public function alterTable()
		{

		}

		/**
		 * Check if the attribute can be used, or if it's an internal attribute
		 *
		 * @return bool
		 */
		public function checkAttrName($key)
		{
			if (
					$key != "id_self" &&
					$key != "pop_db" &&
					$key != "pop_dbtype" &&
					$key != "pop_parent" &&
					$key != "pop_parent_type" &&
					$key != "pop_attrs" &&
					$key != "pop_parent_attrs" &&
					$key != "pop_self_attrs" &&
					$key != "pop_schema" &&
					$key != "pop_table" &&
					$key != "pop_parent_schema" &&
					$key != "pop_parent_table" &&
					$key != "pop_db_driver" &&
					$key != "parent_classes" &&
					$key != "pop_parent_classes" &&
					$key != "db" &&
					$key != "schema" &&
					$key != "table" &&
					$key != "meta_mapping" &&
					$key != "pop_meta_mapping"
				)
				return true;

			return false;
		}

		/**
		 * Get the table name of the parent object
		 *
 		 * @return string
		 */
		public function getParentTable()
		{
			if ($this->pop_parent_type != "Persist")
			{
				return $this->pop_parent->getParentTable();
			}
			else
				return $this->getTargetName();
		}

		/**
		 * Check if the attribute exists with that name
		 *
		 * @return bool
		 */
		public function checkAttrExistence($key)
		{
			foreach ($this->pop_attrs as $var => $objvalue)
				if ($key == $var)
					return true;
			return false;
		}

		/**
		 * Check if the attribute exists with that name on the parent
		 *
		 * @return bool
		 */
		public function checkParentAttrExistence($key)
		{
			foreach ($this->pop_parent_attrs as $var => $parobjvalue)
				if ($key == $var)
					return true;
			return false;
		}

		/**
		 * Get the name of the attribute from the meta mapping
		 *
		 * @return string
		 */
		public function getMetaName($key)
		{
			$key = strtolower($key);
			if ($this->meta_mapping && isset($this->meta_mapping[$key]))
				return $this->meta_mapping[$key];
			else
			{
				if($this->checkParentAttrExistence($key))
                    return $this->pop_parent->getMetaName($key);
                else
                    return $key;
			}
		}

		/**
		 * Get the name of the field from the meta mapping
		 *
		 * @return string
		 */
		public function getRealName($meta)
		{
			$meta = strtolower($meta);
			if ($this->meta_mapping && in_array($meta,$this->meta_mapping))
				return array_search($meta,$this->meta_mapping);
			else
				return $meta;
		}

		/**
		 * Set the PDO DB connection that should be used by the object and get default properties
		 * from the driver, like default schema.
		 *
		 * @param string $pop_db name of the global used for database connection
		 */
		public function setDBConnection()
		{
			if ($this->db)
				$this->pop_db = $this->db;

			POPDB::getConnection($this->pop_db)->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
			$this->pop_dbtype = POPDB::getConnection($this->pop_db)->getAttribute(PDO::ATTR_DRIVER_NAME);

			$this->pop_db_driver = POPDBDriverRegistry::getDriver($this->pop_dbtype);
		}

		/**
		 * Get the PDO DB connection name in use
		 *
		 * @return string
		 */
		public function getDBConnection()
		{
			return $this->pop_db;
		}

		/**
		 * Get the name of the schema from the meta mapping
		 *
		 * @return string
		 */
		public function getSchemaName()
		{
			if ($this->schema)
				$this->pop_schema = $this->schema;
			else
				$this->schema = $this->pop_schema = strtolower($this->pop_db_driver->schema);

			return $this->pop_schema;
		}

		/**
		 * Get the name of the table from the meta mapping
		 *
		 * @return string
		 */
		public function getTableName()
		{
			if ($this->table)
				$this->pop_table = $this->table;
			else
				$this->table = $this->pop_table = strtolower(get_class($this));

			return $this->pop_table;
		}

		/**
		 * Get the target name (schema + table)
		 */
		public function getTargetName()
		{
			if ($this->pop_schema)
				return $this->pop_schema.$this->pop_db_driver->separator.$this->pop_table;
			else
				return $this->pop_table;
		}

		/**
		 * Get the attributes mapped on parent
		 *
		 * @return mixed
		 */
		public function getParentMetaAttrs($arr = array())
		{
			if ($this->pop_parent_type != "Persist")
			{
				if (!isset($arr[$this->pop_parent->getTargetName()]))
					$arr[$this->pop_parent->getTargetName()] = array();

				$temp_arr = $this->pop_parent->pop_self_attrs;

				foreach ($temp_arr as $key=>$value)
					if ($this->pop_parent->checkAttrName($key))
						$arr[$this->pop_parent->getTargetName()][$key] = $this->pop_parent->getMetaName($key);

				return $this->pop_parent->getParentMetaAttrs($arr);
			}
			else
				return $arr;
		}

		/**
		 * Initializes the class
		 */
		public function initialize()
		{
			//object will use the Global PDO DB connection created before
			$this->setDBConnection();
			//get the schema name to be used
			$this->getSchemaName();
			//get the table name to be used
			$this->getTableName();

			$this->pop_attrs = get_object_vars($this);
			$this->pop_parent_attrs = array();
			$this->pop_self_attrs = array();

			//if has parent and it is not Persist, we must have a way to get it
			$this->pop_parent_type = get_parent_class($this);

			if ($this->pop_parent_type != "Persist")
			{
				$this->pop_parent = new $this->pop_parent_type();
				$this->pop_parent_attrs	= get_object_vars($this->pop_parent);
				$this->pop_parent_table = strtolower($this->pop_parent->getTableName());
				$this->pop_parent_schema = strtolower($this->pop_parent->getSchemaName());

				if ($this->pop_parent->parent_classes == $this->parent_classes)
					$this->parent_classes = array();
			}

			$this->pop_self_attrs = array_diff_key($this->pop_attrs, $this->pop_parent_attrs,array("id"=>""));

			POPDebug::info('Object of type '.get_class($this).' instantiated');
		}
	}

?>